Skip to content

DR-SEC-01g-G1: Denkraum ledger ownership primitive (option A) - #811

Merged
mergify[bot] merged 9 commits into
mainfrom
cursor/dr-sec-01g-g1-ownership
Oct 9, 2026
Merged

mergify[bot] merged 9 commits into
mainfrom
cursor/dr-sec-01g-g1-ownership

Conversation

@Cuarroc

@Cuarroc Cuarroc commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Es gibt jetzt eine eigene Sperrdatei neben dem Ledger: nur ein Schreibprozess darf sie zuerst anlegen. Ein zweiter Prozess oder eine alte/kaputte Sperre wird abgelehnt, ohne dass jemand die Sperre stillschweigend übernimmt. Nach Elias E6b ist „ein Nonce nur einmal freigeben“ dokumentierte API-Vorbedingung; Restrisiko und ehrlicher Testkommentar sind festgehalten — Delta-Check durch Root steht noch aus, noch nichts mergen.

Report

Package: DR-SEC-01g-G1 · Review tier: A (security/concurrency) · Model: Cursor auto
Base: origin/main @ 15305c2e1eb503df4834f5cdce1a372f3c39964c
Head: 4c56941acfbdc2d05f5641fd031a3e823b3764f6
Goal / benefit: Standalone exclusive-writer ownership primitive (option A) before core/server wiring.
Parent / ticket / owner: DR-SEC-01g / Root decision 2026-10-09 option A / package G1
Native evidence: Cursor CreateGoal is only for explicit user requests here; ticket goal from Root brief used as documented contract.
Scope / limits: New module + tests + child fixture only; not wired into core.mjs/server.mjs; no application-protection claim; size 300 lines (cap 300).
Dependencies / acceptance: Design report Option A + G1 row; node --test tools/denkraum/store/ownership.test.mjs exit 0; npm run test:denkraum exit 0; bash scripts/ci/gates.sh lane prepush exit 0.

What changed

  • Added tools/denkraum/store/ownership.mjs: exclusive create (wx, mode 0600) of <ledger>.owner with nonce/PID/heartbeat; fixed diagnostics OWNERSHIP_HELD, OWNERSHIP_MALFORMED, OWNERSHIP_RELEASE_MISMATCH, OWNERSHIP_RECOVERY_REFUSED (message equals code, no paths).
  • Fail-closed: existing valid/stale/suspended owner refuses second acquire; malformed/empty record refuses without deletion; recoverOwnership always refuses (G1 stub).
  • Release deletes only when nonce matches; foreign nonce keeps the record.
  • Added ownership.test.mjs (real child process, no sleep >2s) and ownership-contender.fixture.mjs.
  • No changes to store/core.mjs, delivery.mjs, server.mjs, cli.mjs, or config.mjs.

Architecture drift

  • No new findings; no allowlist edits.

Prompt-Log

  • rounds: 1 · critic: self · findings: 0 high / 0 other · changed: none after first green
  • Draft: Option A exclusive sidecar lock with nonce-checked release and refuse-only recovery stub.
  • Critique: no defect found against G1 acceptance (second process, stale, malformed, foreign release).
  • Final: 225-line candidate as committed.

Evidence

Command Exit code
RED before module: node --test tools/denkraum/store/ownership.test.mjs (ERR_MODULE_NOT_FOUND) 1
node --test tools/denkraum/store/ownership.test.mjs 0
npm run test:denkraum 0
bash scripts/ci/red-first.sh 0
bash scripts/ci/gates.sh lane prepush 0
git push -u origin cursor/dr-sec-01g-g1-ownership 0
git ls-remote origin cursor/dr-sec-01g-g1-ownership → 24f0b157042b17bbcaaa0df0d47446d349ef6a2d 0

Diff size: 3 files, +225 / −0 (under 280-line cap).

NICHT ABGEDECKT

  • Linux only: #[cfg(windows)] Rust tests and Windows clippy arms do not compile/run here; Windows verdict is the merge-queue gates (windows) run.
  • Mode 0600 assertion is POSIX-skipped on Windows (POSIX mode 0600 is not Windows ACL evidence); Windows ACL/handle/alias recovery remains for G6.
  • Module is not wired into core/delivery/server; concurrent writers of the live app are still unprotected (G2–G4).
  • No automated stale takeover, no NFS/SMB/multi-host guarantee, no production recovery execution.
  • Operator recovery beyond the refusing stub is out of scope.

Fix round 1 (NACHBESSERN)

Accepted round-1 findings with one commit each (test + fix). Assertion-RED recorded on head 24f0b157042b before the fixes (not import-error RED). R811-O3 (original Test-First was import-error RED at merge-base) is not repaired by rewriting history; the fix-round tests supply assertion RED on 24f0b157.

Command on 24f0b157 (module/fixture restored; new tests kept) Exit
node --test --test-name-pattern '^DRSEC: overlapping release cannot unlink a successor record$' … 1 (AssertionError: Missing expected rejection)
node --test --test-name-pattern '^DRSEC: filesystem failures use fixed diagnostics without paths$' … 1 (AssertionError: validation false — raw path-bearing FS error)
node --test --test-name-pattern '^DRSEC: failed write after create removes the half-written record$' … 1 (AssertionError: validation false — raw ENOSPC / record left)
node --test --test-name-pattern '^DRSEC: fixture exits non-zero when stdin closes without release$' … 1 (AssertionError: Expected actual ≠ 0)
node --test --test-name-pattern '^DRSEC: release of a missing record is a fixed mismatch$' … 0 (already correct on 24f0b157; coverage pin)
Command at head cbadc7127484 Exit
node --test tools/denkraum/store/ownership.test.mjs 0 (11 pass)
node ~/pa-orch/reports/rv811-openai-probe.mjs (adapted outside repo) 0 (O1 successor kept; O2 path-free OWNERSHIP_IO)
npm run test:denkraum 0 (206 pass, 1 skip)
bash scripts/ci/red-first.sh 0
bash scripts/ci/gates.sh lane prepush 0
git ls-remote origin cursor/dr-sec-01g-g1-ownership → cbadc7127484bb35230d820dda20fb62f8fc7946 0

Diff size vs origin/main: 3 files, +298 / −0 (≤300).

Reviews and disposition

ID Source Severity Finding Disposition
R811-O1 / R811-B1 openai reject / anthropic high / low-medium Overlapping releases can unlink a successor accepted + 2bf5b08526da8d72fc225c8477c648e8eff88968 (single-flight + tombstone)
R811-O2 / R811-A1 openai / anthropic medium FS failures leak paths / raw errors accepted + 6eab0fe6ef80791343ca52755271a49eaeff843a (OWNERSHIP_IO, no cause/path)
R811-A2 anthropic medium Failed write after wx leaves empty lock accepted + 896ff3f5a6a5883f13c930cf00ca2eeea1975d47 (creator unlinks half-written)
R811-O4 / R811-B3 openai / anthropic medium / low Fixture cleanup not failure-safe; stdin close exits 0 accepted + a2883e9ec44f04452a120912122a8a6655393248
GLM F1 anthropic conditions medium Missing-record release coverage accepted + cbadc7127484bb35230d820dda20fb62f8fc7946 (test pin)
R811-O3 openai medium Original Test-First was import-error RED rejected rewrite of history; fixed by assertion-RED fix-round tests above
R811-B2 anthropic low No SIGKILL/SIGSTOP child proof rejected for G1 (out of scope; G6)
R811-C1 anthropic info MALFORMED vs HELD during create window rejected for G1 (still refuse, no delete)
R811-O5 / R811-F1 openai r2 / anthropic final high / medium Cross-instance stale release / same root cause accepted residual risk per Elias E6b (documented precondition), G2 enforces + 4c56941acfbdc2d05f5641fd031a3e823b3764f6
R811-F2 anthropic final low Test name claimed "never" / never two owners accepted residual risk per Elias E6b (documented precondition), G2 enforces + 4c56941acfbdc2d05f5641fd031a3e823b3764f6 (name kept for red-first trailer; clarifying comment only)
— — — Review: ausstehend (Stufe A) delta check pending Root (E6b doc + test comment at 4c56941)

Fix round 3 (NACHBESSERN, only R811-O5)

Round 3 approved by Elias (E6) 2026-10-09; delta reviews OpenAI + Anthropic + GLM pending Root. Review: ausstehend (Stufe A).

Command Exit
Assertion-RED on cbadc7127484 (hooks-only restore=rename): node --test --test-name-pattern '^DRSEC: cross-instance stale release never overwrites a live owner$' tools/denkraum/store/ownership.test.mjs 1 (ERR_ASSERTION: third overwritten by successor)
node --test tools/denkraum/store/ownership.test.mjs @ 57a0c811e0d0a4 0 (12 pass)
npm run test:denkraum 0 (207 pass, 1 skip)
bash scripts/ci/red-first.sh 0
bash scripts/ci/gates.sh lane prepush 0
git ls-remote origin cursor/dr-sec-01g-g1-ownership → 57a0c811e0d0a41472d68d49e82640ac1d9e9116 0

Diff size vs origin/main: 3 files, +296 / −0 (≤300). Fix: unique per-release tombstone (nonce+random); on mismatch restore only via exclusive link (never rename over a live path). Fixture trimmed for size budget; all behavioural assertions kept.

Fix round E6b (NACHBESSERN, doc + test comment only)

Elias E6b (2026-10-09, „angenommen“): each release nonce may be released only once becomes a documented API precondition; residual risk of a second stale cross-instance release is accepted for G1; G2 (core) will enforce single release. No logic change.

Item Disposition
R811-O5 / R811-F1 accepted residual risk per Elias E6b (documented precondition), G2 enforces + 4c56941acfbdc2d05f5641fd031a3e823b3764f6
R811-F2 accepted residual risk per Elias E6b (documented precondition), G2 enforces + 4c56941acfbdc2d05f5641fd031a3e823b3764f6 (honest clarifying comment; original test name kept because renaming broke red-first against the earlier Test-First: trailer)
Command Exit
node --test tools/denkraum/store/ownership.test.mjs 0 (12 pass)
bash scripts/ci/red-first.sh (after rename attempt) 1 (old trailer name missing at head) → name restored
bash scripts/ci/red-first.sh @ 4c56941 0
bash scripts/ci/gates.sh lane prepush 0
git ls-remote origin cursor/dr-sec-01g-g1-ownership → 4c56941acfbdc2d05f5641fd031a3e823b3764f6 0

Diff size vs origin/main: 3 files, +300 / −0 (cap 300). Commits: 582823e (precondition comment + rename attempt), 4c56941 (restore name for red-first). Review: delta check pending Root.

Open points

  • State: E6b applied (documented single-release precondition + clarifying test comment); residual R811-O5/F1/F2 accepted per Elias; Review: delta check pending Root; not merged; not usable as app protection yet.
  • Handover: candidate 4c56941acfbdc2 on base 15305c2; next owner Root for delta check, then G2 enforces single release when owners clear.
  • Licenses: no new dependencies.
  • Model: Cursor auto (served model not exposed).

Made with Cursor

Root-Stand nach Runde 2 (2026-10-09, head cbadc71)

ID Quelle Schwere Befund Status
R811-O1 OpenAI r1 high overlapping release unlinks a successor fixed for one module instance (releaseFlight); see O5
R811-O2/A1 OpenAI/Anthropic r1 medium raw fs errors leak paths fixed (OWNERSHIP_IO, no cause)
R811-A2 Anthropic r1 medium half-written record wedges the lock fixed for caught write failure; crash case → G6
R811-O4/B3 OpenAI/Anthropic r1 medium fixture cleanup could hang fixed
R811-O3 OpenAI r1 medium original six tests only import-error RED partly: four new tests show assertion RED; history not rewritten (rule)
R811-O5 OpenAI r2 review-pr811-openai-r2-cbadc7127484.md high ownership.mjs:66,75,82-89: cross-instance stale release overwrite accepted + 57a0c811e0d0a41472d68d49e82640ac1d9e9116
R811-A1 (r2) Anthropic r2 low in G1 / medium once recovery exists same restore race, reachable only via replacement open
R811-A2/A3 (r2), GLM F1–F5 Anthropic r2, GLM low waiter reports MISMATCH after IO failure; 2 s timers; tomb orphan on read failure; Windows rename-over-existing open, low

Verdicts round 2: OpenAI reject (O5 high), Anthropic approve with conditions, GLM approve with conditions. E6 approved Round 3 (only O5); fixed at 57a0c811e0d0a4 (size 296/300). Delta reviews OpenAI + Anthropic + GLM pending Root. Module is not wired into core/server yet. Review: ausstehend (Stufe A).

Root: Runde 3 (head 57a0c81, 2026-10-09) — Nutzerentscheidung E6b

Round 3 approved by Elias (E6, „allen Empfehlungen folgen“), only for R811-O5. Fix 57a0c81 (unique tombstone + exclusive link restore, 296/300).

Reviewer Report Verdict
OpenAI (Codex gpt-6-astra) review-pr811-openai-final-57a0c811e0d0.md reject: R811-O5 not resolved — ownership.mjs:65 still moves a live successor out of the admission path; :75-76 discard the successor record on EEXIST → two owners possible
Anthropic (Sonnet 5.5) review-pr811-anthropic-final-57a0c811e0d0.md reject, with a downgrade path: approve with conditions if "one release per nonce" becomes a documented API precondition (G1 is unwired; a duplicate stale release is a caller bug); R811-F1 medium (same root cause), F2 low (test name claims "never")
GLM 5.2 local prompt lists exactly the 3 PR files approve (F1/F2 low: leaked tombstone on unlink failure, misleading IO diagnostic)

Overwrite of a third owner's record: resolved (both). Double admission via two stale releases with the same nonce from two module instances: accepted residual risk per Elias E6b (documented precondition), G2 enforces — applied at 4c56941 (API comment + honest test comment; rename reverted to keep red-first). Review: delta check pending Root.

Root-Abnahme (head 4c56941, 2026-10-09 13:20 UTC)

Check Result
Delta 57a0c81..4c56941 2 files, +4/−0, comments only (API precondition + test comment); no logic change → Tier C delta, no further review round
Size 300/300
Prepush (worker, server) exit 0; red-first exit 0 at 4c56941
R811-O5 / F1 / F2 accepted residual risk per Elias E6b ("angenommen", 2026-10-09 ~12:40Z); single release per nonce becomes a documented precondition
Deviation from E6b test rename reverted: the Test-First: trailer pins the old name, red-first fails on a rename. Honest clarifying comment instead; the rename moves to G2 (with No-Test:), together with core enforcement of single release
Wiring module not wired into core/server; no runtime effect until G2

NICHT ABGEDECKT: double admission through two stale releases of the same nonce from two module instances (accepted until G2); Windows half comes from the merge queue.

Introduce a standalone Option-A ownership module with O_EXCL create,
nonce-checked release, and fail-closed stale/malformed refusal before
core/server wiring.

Test-First: tools/denkraum/store/ownership.test.mjs::DRSEC: second process is refused while ownership is held
Test-First: tools/denkraum/store/ownership.test.mjs::DRSEC: same-process second acquire with a different nonce is refused
Test-First: tools/denkraum/store/ownership.test.mjs::DRSEC: stale heartbeat does not release ownership
Test-First: tools/denkraum/store/ownership.test.mjs::DRSEC: malformed owner record fails closed
Test-First: tools/denkraum/store/ownership.test.mjs::DRSEC: release with foreign nonce keeps the record
Test-First: tools/denkraum/store/ownership.test.mjs::DRSEC: owner record is created exclusively with mode 0600
Co-authored-by: Cursor <cursoragent@cursor.com>
@mergify

mergify Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 All 2 merge protections satisfied — ready to merge.

Show 2 satisfied protections

🟢 Paket-PR bringt seinen Bericht mit

Package branches (w<N>-, df<N>, ki-<N>, hq2-, dr-, z<N>-) must carry their report in the PR body, in a section that starts with "## Report".

  • body ~= (?m)^## Report

🟢 🚦 Auto-queue

When all merge protections are satisfied and these conditions match, this pull request will be queued automatically.

  • -conflict
  • -draft
  • base = main
  • check-success = gates (linux)
  • check-success = gates (windows)
  • check-success = red-first
  • label != do-not-merge
  • label = review-ok

Cuarroc and others added 5 commits October 9, 2026 05:03
Overlapping release with the same nonce no longer deletes a successor's
owner record: release is single-flight per nonce and unlinks only after a
verified tombstone rename.

Test-First: tools/denkraum/store/ownership.test.mjs::DRSEC: overlapping release cannot unlink a successor record
Co-authored-by: Cursor <cursoragent@cursor.com>
…IP_IO

Acquire and release normalize open/read/rename/unlink failures to a fixed
OwnershipError code without attaching the raw cause or any path fragment.

Test-First: tools/denkraum/store/ownership.test.mjs::DRSEC: filesystem failures use fixed diagnostics without paths
Co-authored-by: Cursor <cursoragent@cursor.com>
…ilure

When wx create succeeds but the initial write/flush fails, the creator
unlinks its own incomplete record and returns a fixed OWNERSHIP_IO error.

Test-First: tools/denkraum/store/ownership.test.mjs::DRSEC: failed write after create removes the half-written record
Co-authored-by: Cursor <cursoragent@cursor.com>
…n close

Register child cleanup immediately after spawn, join with a deadline on
every path, and make the contender fixture exit non-zero if stdin closes
without an explicit release.

Test-First: tools/denkraum/store/ownership.test.mjs::DRSEC: fixture exits non-zero when stdin closes without release
Co-authored-by: Cursor <cursoragent@cursor.com>
Pins the missing-record release path to OWNERSHIP_RELEASE_MISMATCH with
path-free diagnostics (GLM F1).

Test-First: tools/denkraum/store/ownership.test.mjs::DRSEC: release of a missing record is a fixed mismatch
Co-authored-by: Cursor <cursoragent@cursor.com>
Cuarroc and others added 3 commits October 9, 2026 13:19
Unique per-release tombstones and exclusive link restore prevent a stale
cross-instance release from overwriting a live successor (R811-O5).

Test-First: tools/denkraum/store/ownership.test.mjs::DRSEC: cross-instance stale release never overwrites a live owner
Co-authored-by: Cursor <cursoragent@cursor.com>
No-Test: documentation of the accepted API precondition and an honest test name (Elias E6b)
Co-authored-by: Cursor <cursoragent@cursor.com>
No-Test: documentation of the accepted API precondition and an honest test name (Elias E6b)
Co-authored-by: Cursor <cursoragent@cursor.com>
@Cuarroc
Cuarroc marked this pull request as ready for review October 9, 2026 13:12
@Cuarroc Cuarroc added the review-ok Pipeline: review disposition complete, no open high finding label Oct 9, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-09T13:19:33.418302Z 4c56941 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@mergify mergify Bot mentioned this pull request Oct 9, 2026
34 of 50 tasks

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c56941acf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

export class OwnershipError extends Error {
constructor(code) { super(code); this.name = 'OwnershipError'; this.code = code; }
}
export const ownerRecordPath = ledgerPath => `${ledgerPath}.owner`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent aliases from creating independent owner files

When the same existing ledger is supplied through a file symlink or hard-link alias, this derives different .owner paths from the two lexical names, so both acquireOwnership calls succeed and two servers are admitted. This is reachable through normal configuration because loadStartConfig preserves the lexical statePath after only using the physical path for validation; derive ownership from a stable physical identity or reject such aliases before G2 relies on this primitive.

Useful? React with 👍 / 👎.

}
const record = parseRecord(raw);
if (!record || record.nonce !== nonce) throw new OwnershipError(OWNERSHIP_RELEASE_MISMATCH);
const tomb = `${path}.${nonce}.${randomBytes(8).toString('hex')}.tomb`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep tombstone names within filesystem component limits

For a valid ledger basename near the filesystem's component limit, acquisition succeeds because the owner file adds only .owner, but release appends another 55 characters for this tombstone and fails with OWNERSHIP_IO/ENAMETOOLONG, leaving the owner record permanently held. On a filesystem with a 255-byte component limit this already occurs for a 200-character basename, while the existing store can still write that ledger; use a bounded tombstone name in the same directory rather than extending the full owner filename.

Useful? React with 👍 / 👎.

const nonce = randomBytes(16).toString('hex');
const record = { nonce, pid: process.pid, heartbeatAt: new Date().toISOString() };
let handle;
try { handle = await open(path, 'wx', 0o600); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Create the ledger directory before acquiring ownership

When the configured state file's parent directory does not yet exist, this open returns ENOENT and acquisition reports OWNERSHIP_IO. That state path is accepted by loadStartConfig, and DeskStore.change currently supports it by creating the parent recursively before the first write, so acquiring ownership before that write will make a fresh state location fail to start; create the parent during acquisition or require an explicit setup step before this call.

Useful? React with 👍 / 👎.

@mergify mergify Bot added the queued label Oct 9, 2026
@mergify
mergify Bot merged commit b19d1bb into main Oct 9, 2026
17 checks passed
@mergify
mergify Bot deleted the cursor/dr-sec-01g-g1-ownership branch October 9, 2026 13:37
@mergify

mergify Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

This pull request spent 18 minutes 12 seconds in the queue, including 17 minutes 55 seconds running CI.

Required conditions to merge
  • check-success = gates (linux)
  • check-success = gates (windows)
  • check-success = red-first
  • github-review-approved [🛡 GitHub repository ruleset rule main-pr-required-checks]
  • label != do-not-merge
  • all of [🛡 Merge Protections rule Paket-PR bringt seinen Bericht mit]:
  • any of [🛡 GitHub repository ruleset rule main-pr-required-checks]:
    • check-success = @github-actions/gates (linux)
    • check-neutral = @github-actions/gates (linux)
    • check-skipped = @github-actions/gates (linux)
  • any of [🛡 GitHub repository ruleset rule main-pr-required-checks]:
    • check-success = @github-actions/gates (windows)
    • check-neutral = @github-actions/gates (windows)
    • check-skipped = @github-actions/gates (windows)
  • any of [🛡 GitHub repository ruleset rule main-pr-required-checks]:
    • check-success = @github-actions/red-first
    • check-neutral = @github-actions/red-first
    • check-skipped = @github-actions/red-first
  • any of [🛡 GitHub repository ruleset rule main-pr-required-checks]:
    • check-neutral = @mergify/Mergify Merge Protections
    • check-skipped = @mergify/Mergify Merge Protections
    • check-success = @mergify/Mergify Merge Protections

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-ok Pipeline: review disposition complete, no open high finding

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant